""" 国控点大数据分析管理软件GkdToolkit主程序（图形界面）
    发布版本1.0对应内部版本5.0，之后以发布版本为准 """

import os
import sys
import traceback
import subprocess
import numpy as np
import pandas as pd
from functools import partial

from PyQt6.QtCore import Qt, QDate, pyqtSignal, QThread, QUrl
from PyQt6.QtGui import QAction, QFont, QIcon, QDesktopServices
from PyQt6.QtWidgets import QMainWindow, QWidget, QGroupBox, QStyle, \
    QDateEdit, QComboBox, QSizePolicy, QSpacerItem, \
    QGridLayout, QFormLayout, QHBoxLayout, QVBoxLayout, \
    QStatusBar, QLabel, QPushButton, QListWidget, QAbstractItemView, \
    QSpinBox, QFileDialog, QApplication, QMessageBox, QMenu, QProgressDialog

import _config as cfg
import _gkd as gkd
import _plot as plot
import _plot_html as plot_html


class GkdGui(QMainWindow):
    """ 主界面GUI """

    def __init__(self):
        super().__init__()

        """ 其它参数 """
        # 当前选中的区域级别： site/city/province
        self.region_level = 'site'

        # 数据统计有效性标准
        self.dict_stat_standard = dict()

        # 载入数据
        self.thread_get_data = None

        # 进度条初始化
        self.progress = None

        # 时间分辨率：'H', 'D', 'MS', 'QS-MAR', 'YS'
        self.time_resolution = 'H'

        # 当前操作是提取数据，还是作图
        self.current_operation = 'data'

        # 当前界面日期
        # self.dt_min, self.dt_max = None, None

        # 运行程序启动检查
        # self.initial_check()

        # 检查NewSiteList.csv文件是否存在
        if not os.path.exists(cfg.path_site_list):
            self.show_error('未在%s目录找到NewSiteList.csv文件!' % cfg.dir_hdf5)

        # 读取站点信息(V4.1中新增)
        self.si = gkd.SiteInfo()

        # 窗口图标
        self.setWindowIcon(QIcon(cfg.path_logo))

        # 窗口标题
        self.setWindowTitle("国控点大数据分析管理软件「GkdToolkit1.1」")

        # 窗口尺寸
        self.resize(1200, 680)

        # 居中显示界面
        # center_point = QDesktopWidget().availableGeometry().center()
        # self.move(center_point.x() - int(590 * self.lsf), center_point.y() - int(337 * self.lsf))

        # 10号字
        self.font_10 = QFont('Microsoft YaHei UI', 10)
        self.setFont(self.font_10)

        # 12号子
        # self.font_12 = QFont('Microsoft YaHei UI', 11)

        # # 读取站点信息(V4.1中新增)
        # self.si = gkd.SiteInfo()
        #

        # 读取数据库时间范围
        self.get_datetime_range()

        # 弹簧
        self.vspacer = QSpacerItem(40, 20, QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)  # 垂直QSpacer
        self.hspacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)  # 垂直QSpacer

        """ 菜单栏初始化 """
        self.init_menu()

        """ 状态栏初始化 """
        # 状态栏文字
        self.status_text = QLabel()
        self.init_statusbar()

        # 主界面Widget
        self.main_widget = QWidget(self)
        self.setCentralWidget(self.main_widget)

        # 主界面Layout
        self.gridLayout_main = QGridLayout(self.main_widget)
        self.gridLayout_main.setContentsMargins(10, 4, 10, 0)  # 左上右下边距
        # self.gridLayout_main.setContentsMargins(round(self.lsf * 10), round(self.lsf * 4), round(self.lsf * 10), 0)  # 左上右下边距
        # self.gridLayout_main.setHorizontalSpacing(round(self.lsf * 6))

        """ 物种选择列表控件初始化 """
        self.init_listwidget_species()

        """ 省级行政区选择列表控件初始化 """
        # label
        self.label_province = QLabel('省(' + str(len(self.si.list_province_china)) + ')')

        # 初始化
        self.init_listwidget_province()

        """ 市级行政区选择列表控件初始化 """
        # Label
        self.label_city = QLabel('地市(0)')

        # 初始化
        self.init_listwidget_city()

        """ 站点选择列表控件初始化 """
        # Label
        self.label_site = QLabel('站点(0)')

        # 初始化
        self.init_listwidget_site()

        """ 待导出列表控件初始化 """
        # label
        self.label_export = QLabel('待导出(0)')

        # 初始化
        self.init_listwidget_candidate()

        """ 选项Layout: 时间范围、区域级别、时间分辨率 """
        self.vlayout_option = QVBoxLayout()

        # 日期范围选择控件
        self.init_daterange()

        # 数据有效性要求输入控件初始化
        self.init_statistical_validity()

        # 站点类型选择控件初始化
        self.init_site_type()

        # 时间分辨率选择控件初始化
        self.init_time_resolution()

        # 导出区域级别选择控件初始化
        self.init_region_level()

        # 浓度评价标准选择控件初始化
        self.init_stat_standard()

        # 分位数输入框LineEdit
        # self.lineEdit_quantile = QLineEdit()
        # self.lineEdit_quantile.setPlaceholderText('在此输入分位数值(0-100)')
        # self.lineEdit_quantile.setVisible(False)
        # self.vlayout_option.addWidget(self.lineEdit_quantile)

        # 涉及O3的计算时是否计算MDA8
        self.init_o3_mda8()

        """ 运行按钮 """
        self.pushButton_run = QPushButton('提取数据')

        # 固定高度
        self.pushButton_run.setFixedHeight(40)

        # slot
        self.pushButton_run.clicked.connect(self.slot_click_load_data)

        # 添加至vlayout
        self.vlayout_option.addItem(QSpacerItem(40, 20, QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding))
        self.vlayout_option.addWidget(self.pushButton_run)

        # 添加至主界面layout
        self.gridLayout_main.addLayout(self.vlayout_option, 0, 5, 10, 1)

        # 主界面各垂直Layout占比
        self.gridLayout_main.setColumnStretch(0, 3)  # 物种选择控件
        self.gridLayout_main.setColumnStretch(1, 6)  # 省级行政区选择控件
        self.gridLayout_main.setColumnStretch(2, 9)  # 地市级行政区选择控件
        self.gridLayout_main.setColumnStretch(3, 16)  # 站点选择控件
        self.gridLayout_main.setColumnStretch(4, 16)  # 待导出数据控件
        self.gridLayout_main.setColumnStretch(5, 4)  # 功能选项控件

    def initial_check(self):
        """ 程序运行检查 """

        # 检查NewSiteList.csv文件是否存在
        if not os.path.exists(cfg.path_site_list):
            print('dddd222')
            self.show_error('未在%s目录找到NewSiteList.csv文件!' % cfg.dir_hdf5)

        # 读取站点信息(V4.1中新增)
        self.si = gkd.SiteInfo()

        """ 数据库完整性检查 """
        # 缺失的h5数据列表
        list_h5_missing = [s for s in cfg.list_species if not os.path.exists(os.path.join(cfg.dir_hdf5, s + '.h5'))]

        if list_h5_missing:

            # 对话框询问是否重建数据库
            result = YesOrNo(window_title='询问', tips='hdf5数据库缺失: %s。' % ', '.join(list_h5_missing) + '\n\n是否重建数据库？').exec()

            # 重建
            if result == "是":

                self.menu_csv2h5()

            # 退出程序
            else:
                pass

        # 读取数据库时间范围
        self.dt_min, self.dt_max = gkd.get_datetime_range()

        # 状态栏初始化
        # self.init_statusbar()

    def slot_update_time_resolution(self):
        """ 更新时间分辨率, self.comboBox_tr变化时触发 """

        # 时间分辨率
        time_resolution = self.comboBox_tr.currentText()

        if time_resolution == '原始':
            self.time_resolution = 'H'
        elif time_resolution == '日':
            self.time_resolution = 'D'
        elif time_resolution == '月':
            self.time_resolution = 'MS'
        elif time_resolution == '季':
            self.time_resolution = 'QS-MAR'
        elif time_resolution == '年':
            self.time_resolution = 'YS'
        else:
            self.show_error(error='时间分辨率错误！')

        # 当时间分辨率为H时浓度评价标准仅可选均值
        if self.time_resolution in ['H', 'D']:
            self.comboBox_stat_standard.setCurrentIndex(0)
            self.comboBox_stat_standard.setEnabled(False)
        else:
            self.comboBox_stat_standard.setEnabled(True)

    def menu_csv2h5(self):
        """ 菜单栏点击 """

        # 获得csv数据所在目录
        dir_csv = QFileDialog.getExistingDirectory(self, "选择国控点数据（*.csv）所在目录", cfg.dir_csv)
        # print(dir_csv)

        if not dir_csv:
            return

        # 判断hdf5文件夹是否存在，不存在则新建
        if not os.path.exists(cfg.dir_hdf5):
            os.makedirs(cfg.dir_hdf5)

        # 开启进度条
        self.show_progress_bar(fuzzy=False)

        self.thread_csv2h5 = gkd.Csv2Hdf(dir_csv=dir_csv, dir_h5=cfg.dir_hdf5)

        self.thread_csv2h5.signal_progress.connect(self.after_csv2h5)
        # self.thread.signal_progress.connect(self.update_progress_bar)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_csv2h5.terminate())

        self.thread_csv2h5.start()

    def init_menu(self):
        """ 菜单栏初始化 """

        # 新建菜单
        menu = self.menuBar()

        ''' 新建一级菜单：文件 '''
        # menu_file = menu.addMenu('文件')

        # 保存数据
        # menu_file.addAction('保存数据')

        # 二级菜单：打开文件（*.csv）
        # menu_file.addAction('载入数浓度粒径分布数据 (*.csv;*.xlsx)')
        # menu_file.addAction('保存当前界面数据 (*.csv)')
        # self.menu_file.addAction('保存所有界面合并数据 (*.csv)')
        # self.action_open_csv =

        # 二级菜单：打开数据库
        ''' 数据库 '''
        menu_db = menu.addMenu('数据库')

        # 二级菜单：下载原始数据
        menu_download_raw_csv = QMenu('下载原始数据(*.csv)', self)
        menu_db.addMenu(menu_download_raw_csv)

        # 三级菜单：下载原始数据-从https://quotsoft.net/air/, 镜像
        action_dl_csv_raw = QAction('https://quotsoft.net/air/', self)
        action_dl_csv_mirror = QAction('IPV6镜像', self)
        menu_download_raw_csv.addAction(action_dl_csv_raw)
        menu_download_raw_csv.addAction(action_dl_csv_mirror)
        action_dl_csv_raw.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://quotsoft.net/air/')))
        action_dl_csv_mirror.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('http://[2400:dd01:1024:118:211:32ff:fe5f:9854]:5000/sharing/BGLfTHXkB')))
        # action_dl_csv_mirror.triggered.connect(lambda: webbrowser.open('http://[2400:dd01:1024:118:211:32ff:fe5f:9854]:5000/sharing/BGLfTHXkB'))

        # 下载原始数据
        # menu_db.addAction('原始数据')
        # menu_db.addAction('原始数据镜像（校园网IPV6）')

        # 添加分割线
        menu_db.addSeparator()

        # gzip数据库
        action_gzip = QAction('重建hdf5数据库', self)
        action_gzip.triggered.connect(self.menu_csv2h5)
        menu_db.addAction(action_gzip)

        # lzf数据库
        # menu_db.addAction('生成lzf数据库')

        ''' 区域快捷选择 '''
        menu_region_select = menu.addMenu('重点区域')

        # 重点区域
        list_regions = ['全国', '74个重点城市', '"2+26"城市', '京津冀', '珠三角', '长三角', '四川盆地', '汾渭平原']

        for r in list_regions:
            # 创建动作
            action_r = QAction(r, self)

            # 动作触发
            action_r.triggered.connect(self.menu_region_select)

            # 动作添加至菜单
            menu_region_select.addAction(action_r)
        

        ''' 快捷操作 '''
        menu_shortcut = menu.addMenu('快捷设置')

        for k, item in cfg.dict_shortcut.items():

            # 创建动作
            action_k = QAction(k, self)

            # 动作出发
            action_k.triggered.connect(partial(self.menu_shortcut, item))

            # 添加至菜单
            menu_shortcut.addAction(action_k)
 
        ''' 作图功能 '''
        menu_plot = menu.addMenu('作图分析')

        # 二级菜单：AQI日历热力图
        action_aqi_calendar = QAction('AQI日历热力图', self)
        menu_plot.addAction(action_aqi_calendar)
        action_aqi_calendar.triggered.connect(self.menu_plot_aqi_calendar)

        # 二级菜单：AQI分城市统计，即优良天统计
        menu_aqi_bar = QMenu('AQI分城市统计', self)
        menu_plot.addMenu(menu_aqi_bar)

        # 三级菜单：AQI分城市统计-每年，即优良天统计
        action_aqi_bar_annual = QAction('逐年', self)
        menu_aqi_bar.addAction(action_aqi_bar_annual)
        action_aqi_bar_annual.triggered.connect(lambda: self.menu_plot_aqi_bar('YS'))

        # 三级菜单：AQI分城市统计-每季，即优良天统计
        action_aqi_bar_seasonal = QAction('逐季节', self)
        menu_aqi_bar.addAction(action_aqi_bar_seasonal)
        action_aqi_bar_seasonal.triggered.connect(lambda: self.menu_plot_aqi_bar('QS-MAR'))

        # 三级菜单：AQI分城市统计-每月，即优良天统计
        action_aqi_bar_daily = QAction('逐月', self)
        menu_aqi_bar.addAction(action_aqi_bar_daily)
        action_aqi_bar_daily.triggered.connect(lambda: self.menu_plot_aqi_bar('MS'))

        # 二级菜单：超标天数统计
        menu_exceed_standard = QMenu('超标天数统计', self)
        menu_plot.addMenu(menu_exceed_standard)

        # 三级菜单：PM2.5超国家二级标准75μg/m3，
        action_pm2p5_exceed_standard = QAction('PM₂.₅日均值超标', self)
        action_o3_exceed_standard = QAction('O₃-MDA8超标', self)
        action_pm2p5_o3_exceed_standard_ii = QAction('PM₂.₅日均值和O₃-MDA8均超标准', self)

        action_pm2p5_exceed_standard.triggered.connect(lambda: self.menu_plot_exceed_standard('PM2.5'))
        action_o3_exceed_standard.triggered.connect(lambda: self.menu_plot_exceed_standard('O3'))
        action_pm2p5_o3_exceed_standard_ii.triggered.connect(lambda: self.menu_plot_exceed_standard('PM2.5+O3'))

        menu_exceed_standard.addAction(action_pm2p5_exceed_standard)
        menu_exceed_standard.addAction(action_o3_exceed_standard)
        menu_exceed_standard.addAction(action_pm2p5_o3_exceed_standard_ii)

        # 二级菜单：首要污染物占比变化趋势
        action_primary_pollutant = QAction('首要污染物占比', self)
        menu_plot.addAction(action_primary_pollutant)
        action_primary_pollutant.triggered.connect(self.menu_plot_primary_pollutant)

        # 二次菜单：年均值趋势-柱状图
        action_annual_bar = QAction('年均值趋势(bokeh)', self)
        action_annual_bar.triggered.connect(self.menu_plot_annual)
        menu_plot.addAction(action_annual_bar)

        # 二级菜单：年均值趋势
        menu_annual = QMenu('年均值趋势', self)
        menu_plot.addMenu(menu_annual)

        # 三级菜单：年均值趋势-物种->省/市/站点
        action_plot_annual_by_species = QAction('物种分组', self)
        action_plot_annual_by_regions = QAction('区域级别分组', self)
        action_plot_annual_by_species.triggered.connect(self.menu_plot_annual_by_species)
        action_plot_annual_by_regions.triggered.connect(self.menu_plot_annual_by_regions)
        # action_gzip.triggered.connect(self.menu_csv2h5)
        menu_annual.addAction(action_plot_annual_by_species)
        menu_annual.addAction(action_plot_annual_by_regions)

        # 二级菜单：Geo逐年均值
        menu_geo_annual_by_species = QMenu('Geo逐年均值', self)
        menu_plot.addMenu(menu_geo_annual_by_species)

        # 三级菜单：Geo年均值变化趋势-分物种
        list2add_geo = ['PM2.5', 'PM10', 'O3-MDA8-Mean', 'O3-MDA8-90th', 'NO2', 'CO-Mean', 'CO-95th', 'SO2']

        for s in list2add_geo:
            action_geo_annual_by_s = QAction(s, self)

            # 使用 functools.partial 创建一个包装函数，传递参数s，避免闭包
            wrapped_function = partial(self.menu_plot_geo_by_species, s, 'YS')
            action_geo_annual_by_s.triggered.connect(wrapped_function)

            menu_geo_annual_by_species.addAction(action_geo_annual_by_s)

        # 二级菜单：Geo逐季均值
        menu_geo_seasonal_by_species = QMenu('Geo逐季均值', self)
        menu_plot.addMenu(menu_geo_seasonal_by_species)

        for s in list2add_geo:
            action_geo_seasonal_by_s = QAction(s, self)

            # 使用 functools.partial 创建一个包装函数，传递参数s，避免闭包
            wrapped_function = partial(self.menu_plot_geo_by_species, s, 'QS-MAR')
            action_geo_seasonal_by_s.triggered.connect(wrapped_function)

            menu_geo_seasonal_by_species.addAction(action_geo_seasonal_by_s)

        # 二级菜单：Geo逐月均值
        menu_geo_monthly_by_species = QMenu('Geo逐月均值', self)
        menu_plot.addMenu(menu_geo_monthly_by_species)

        for s in list2add_geo:
            action_geo_monthly_by_s = QAction(s, self)

            # 使用 functools.partial 创建一个包装函数，传递参数s，避免闭包
            wrapped_function = partial(self.menu_plot_geo_by_species, s, 'MS')
            action_geo_monthly_by_s.triggered.connect(wrapped_function)

            menu_geo_monthly_by_species.addAction(action_geo_monthly_by_s)

        # 二级菜单：时间序列
        menu_time_series = QMenu('时间序列', self)
        menu_plot.addMenu(menu_time_series)

        # 三级菜单：时间序列->
        action_plot_timeseries_by_species = QAction('物种分组', self)
        action_plot_timeseries_by_regions = QAction('区域级别分组', self)
        action_plot_timeseries_by_species.triggered.connect(self.menu_plot_ts_by_species)
        action_plot_timeseries_by_regions.triggered.connect(self.menu_plot_ts_by_regions)
        # action_gzip.triggered.connect(self.menu_csv2h5)
        menu_time_series.addAction(action_plot_timeseries_by_species)
        menu_time_series.addAction(action_plot_timeseries_by_regions)

        ''' 新建一级菜单：界面切换 '''
        # menu_interface = menu.addMenu('界面')
        #
        # # 界面索引
        # list_keys = [i for i in self.dict_functions.keys()]
        #
        # # 循环创建二级菜单
        # for i in list_keys:
        #     action_i = str(i + 1) + '. ' + self.dict_functions[i]['annotation'] + ' ⚡ ' + self.dict_functions[i]['version']
        #     menu_interface.addAction(action_i)

        # 添加分割线
        # self.menu_interface.addSeparator()

        # 二级菜单：测试
        # self.menu_interface.addAction('测试')

        # 菜单点击触发
        # menu_interface.triggered.connect(self.switch_stack_option)

        ''' 新建一级菜单：功能 '''
        # menu_function = menu.addMenu('功能')

        # 二级菜单：作图界面
        # action_stack_plot = QAction('作图界面')
        # menu_function.addAction(action_stack_plot)

        # 二级菜单：合并已选仪器数据
        # action_merge_data = QAction('合并已选仪器数据')
        # menu_function.addAction(action_merge_data)

        ''' 新建一级菜单：帮助 '''
        menu_help = menu.addMenu('帮助')
        # self.menu_help.setFont(self.font)

        # 二级菜单：使用教程
        action_manual = QAction('使用教程', self)
        action_manual.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://docs.qq.com/aio/DWENBYVZIUUhNd1NO')))
        menu_help.addAction(action_manual)

        # 分隔符
        menu_help.addSeparator()

        # 恢复日期范围为默认值
        action_recover_daterange = QAction('恢复日期范围为默认值', self)
        action_recover_daterange.triggered.connect(self.menu_recover_default_dt_range)
        menu_help.addAction(action_recover_daterange)

        # 恢复数据有效性要求为默认值
        action_recover_stat_standard = QAction('恢复数据有效性要求为默认值', self)
        action_recover_stat_standard.triggered.connect(self.menu_recover_default_stat_standard)
        menu_help.addAction(action_recover_stat_standard)

        # 二级菜单：关于
        # action_about = QAction('关于')
        # self.action_about.triggered.connect(lambda: self.click_menu('帮助-关于'))
        # menu_help.addAction(action_about)

    def init_statusbar(self):
        """ 状态栏初始化 """

        # 设置状态栏
        statusBar = QStatusBar(self)
        self.setStatusBar(statusBar)

        # 显示数据库时间范围
        if self.dt_min is not None:
            self.label_status_left = QLabel('  数据库：%s─%s  ▏ ' % (self.dt_min.strftime('%Y.%m.%d'), self.dt_max.strftime('%Y.%m.%d')))
        else:
            self.label_status_left = QLabel('  数据库：%s 目录中未发现！' % cfg.dir_hdf5)

        # 颜色：绿色
        self.label_status_left.setStyleSheet("color: green;")

        # self.status_text = QLabel("<p>测试平台: Windows 11 22H2, Python 3.9.13 64bit, PyQt 5.15.7, Pandas 1.5.1, h5py 3.7.0 | 原始数据"
        #  "<a href='https://quotsoft.net/air/'><span>@王晓磊的主页</span></a></p>")

        # 允许设置外部链接
        # self.status_text.setOpenExternalLinks(True)

        # 居中显示
        self.status_text.setAlignment(Qt.AlignmentFlag.AlignHCenter)

        # 添加QLabel
        # statusBar.addWidget(QLabel(' '), 0)
        statusBar.addWidget(self.label_status_left, 1)
        statusBar.addWidget(self.status_text, 6)

    def init_listwidget_species(self):
        """ 初始化列表控件：物种 """

        # 已有的h5数据列表
        # list_h5_existing = [s for s in cfg.list_species if os.path.exists(os.path.join(cfg.dir_hdf5, s + '.h5'))]

        # Label
        self.label_specie = QLabel('物种')

        # listWidget
        self.listWidget_species = QListWidget()

        # 物种列表
        self.listWidget_species.addItems(self.list_species_existing)

        # 允许多选
        self.listWidget_species.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        # 信号槽
        self.listWidget_species.itemSelectionChanged.connect(self.slot_selected_species)

        # 默认选中第一个元素
        self.listWidget_species.setCurrentRow(0)

        # 垂直Layout
        self.verticalLayout_specie = QVBoxLayout()
        self.verticalLayout_specie.addWidget(self.label_specie)  # label
        self.verticalLayout_specie.addWidget(self.listWidget_species)  # 列表
        self.verticalLayout_specie.setSpacing(3)  # 间距

        # self.listWidget_specie.setFixedWidth(round(self.lsf * 60))
        # 添加至主界面Layout
        self.gridLayout_main.addLayout(self.verticalLayout_specie, 0, 0, 10, 1)

    def init_listwidget_province(self):
        """ 初始化列表控件：省级行政区 """

        # listWidget
        self.listWidget_province = QListWidget()
        # print(self.listWidget_province.contentsMargins().left())

        # 添加省级行政区列表
        self.listWidget_province.addItems(self.si.list_province_china)

        # 允许多选
        self.listWidget_province.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        # slot-选中元素更改时
        self.listWidget_province.itemSelectionChanged.connect(self.slot_itemSelectionChanged_province)
        # self.listWidget_province.itemSelectionChanged.connect(lambda: self.update_paras('lw_province'))

        # 允许弹出右键菜单
        self.listWidget_province.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

        # 右键菜单slot
        self.listWidget_province.customContextMenuRequested.connect(self.context_menu_add2candidate_province)

        # 垂直Layout
        self.verticalLayout_province = QVBoxLayout()
        self.verticalLayout_province.addWidget(self.label_province)  # label
        self.verticalLayout_province.addWidget(self.listWidget_province)  # 列表
        self.verticalLayout_province.setSpacing(3)  # 垂直间距

        # 添加至主界面Layout
        self.gridLayout_main.addLayout(self.verticalLayout_province, 0, 1, 10, 1)

    def init_listwidget_city(self):
        """ 初始化列表控件：地市级行政区 """

        # ListWidget
        self.listWidget_city = QListWidget()

        # 允许多选
        self.listWidget_city.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        # slot-选中元素更改时
        self.listWidget_city.itemSelectionChanged.connect(self.slot_itemSelectionChanged_city)
        # self.listWidget_city.itemSelectionChanged.connect(lambda: self.update_paras('lw_city'))

        # 允许弹出右键菜单
        self.listWidget_city.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

        # 右键菜单slot
        self.listWidget_city.customContextMenuRequested.connect(self.context_menu_add2candidate_city)

        # 垂直Layout
        self.verticalLayout_city = QVBoxLayout()
        self.verticalLayout_city.addWidget(self.label_city)  # Label
        self.verticalLayout_city.addWidget(self.listWidget_city)  # 列表
        self.verticalLayout_city.setSpacing(3)  # 间距

        # 添加至主界面Layout
        self.gridLayout_main.addLayout(self.verticalLayout_city, 0, 2, 10, 1)

    def init_listwidget_site(self):
        """ 初始化列表控件：站点 """

        # ListWidget
        self.listWidget_site = QListWidget()

        # 允许多选
        self.listWidget_site.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        # 允许弹出右键菜单
        self.listWidget_site.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

        # 右键菜单slot
        self.listWidget_site.customContextMenuRequested.connect(self.context_menu_add2candidate_site)

        # 垂直Layout
        self.verticalLayout_site = QVBoxLayout()
        self.verticalLayout_site.addWidget(self.label_site)  # label
        self.verticalLayout_site.addWidget(self.listWidget_site)  # 列表
        self.verticalLayout_site.setSpacing(3)  # 间距

        # 添加至主界面Layout
        self.gridLayout_main.addLayout(self.verticalLayout_site, 0, 3, 10, 1)

    def init_listwidget_candidate(self):
        """ 初始化列表控件：待导出 """

        # ListWidget
        self.listWidget_export = QListWidget()

        # 允许多选
        self.listWidget_export.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)

        # 允许弹出右键菜单
        self.listWidget_export.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

        # 右键菜单slot
        self.listWidget_export.customContextMenuRequested.connect(self.context_menu_delete_candidate)

        # 列表变化时触发
        # self.listWidget_export.itemChanged.connect(self.slot_list2export)

        # 垂直Layout
        self.verticalLayout_export = QVBoxLayout()
        self.verticalLayout_export.addWidget(self.label_export)  # label
        self.verticalLayout_export.addWidget(self.listWidget_export)  # 列表
        self.verticalLayout_export.setSpacing(3)  # 间距

        # 添加至主界面Layout
        self.gridLayout_main.addLayout(self.verticalLayout_export, 0, 4, 10, 1)

    def init_daterange(self):
        """ 初始化日期范围控件 """

        # GroupBox
        self.groupBox_timerange = QGroupBox('日期范围')

        # formlayout
        self.formLayout_timerange = QFormLayout(self.groupBox_timerange)

        ''' 日期Label和DateEdit '''
        # Label
        self.label_start = QLabel('开始')
        self.label_end = QLabel('结束')

        # 组件
        self.daterange_start = QDateEdit()
        self.daterange_end = QDateEdit()

        # 初始日期设置
        if self.dt_min is not None:
            self.set_date_range(self.dt_min, self.dt_max)
        else:
            self.set_date_range(pd.to_datetime('2000-01-01'), pd.to_datetime('2000-01-01'))

        # 设置FormLayout的label和filed
        self.formLayout_timerange.addRow(self.label_start, self.daterange_start)
        self.formLayout_timerange.addRow(self.label_end, self.daterange_end)

        # 显示格式
        self.daterange_start.setDisplayFormat("yyyy/MM/dd")
        self.daterange_end.setDisplayFormat("yyyy/MM/dd")

        # 使用日历选择模式
        self.daterange_start.setCalendarPopup(True)
        self.daterange_end.setCalendarPopup(True)

        # 添加至vlayout_option
        self.vlayout_option.addWidget(self.groupBox_timerange)

    def init_time_resolution(self):
        """ 初始化时间分辨率选择控件 """

        # 水平layout
        hlayout_tr = QHBoxLayout()

        # label
        label_tr = QLabel('时间分辨率:')

        # ComboBox
        self.comboBox_tr = QComboBox()

        # 空间占据原则：水平扩展，垂直默认
        self.comboBox_tr.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        # 添加元素：站点、市、省
        self.comboBox_tr.addItems(['原始', '日', '月', '季', '年'])

        # slot-选择变化时触发
        self.comboBox_tr.currentIndexChanged.connect(self.slot_update_time_resolution)

        # 添加进水平layout
        hlayout_tr.addWidget(label_tr)
        hlayout_tr.addWidget(self.comboBox_tr)

        # 添加至vlayout_option
        self.vlayout_option.addLayout(hlayout_tr)

    def init_region_level(self):
        """ 初始化区域级别选择控件 """

        # 水平layout
        hlayout_region_level = QHBoxLayout()

        # label
        label_region_level = QLabel('导出区域级别:')

        # ComboBox
        self.comboBox_region_level = QComboBox()

        # 空间占据原则：水平扩展，垂直默认
        self.comboBox_region_level.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        # 添加元素：站点、市、省
        self.comboBox_region_level.addItems(['站点', '市', '省'])

        # slot-选择变化时触发
        self.comboBox_region_level.currentIndexChanged.connect(self.slot_region_level)

        # 添加进水平layout
        hlayout_region_level.addWidget(label_region_level)
        hlayout_region_level.addWidget(self.comboBox_region_level)

        # 添加至vlayout_option
        self.vlayout_option.addLayout(hlayout_region_level)

    def init_stat_standard(self):
        """ 初始化浓度评价标准选择控件 """

        # 水平layout
        hlayout_ = QHBoxLayout()

        # label
        label_ = QLabel('浓度评价标准:')

        # ComboBox
        self.comboBox_stat_standard = QComboBox()
        self.comboBox_stat_standard.setEnabled(False)

        # 空间占据原则：水平扩展，垂直默认
        self.comboBox_stat_standard.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        # 添加元素：站点、市、省
        self.comboBox_stat_standard.addItems(['均值', '百分位数'])

        # slot-选择变化时触发
        # self.comboBox_assessment_standard.currentIndexChanged.connect(self.slot_quantile_visible)

        # 添加进水平layout
        hlayout_.addWidget(label_)
        hlayout_.addWidget(self.comboBox_stat_standard)

        # 添加至vlayout_option
        self.vlayout_option.addLayout(hlayout_)

    def get_stat_standard(self):
        """ 获取数据统计有效性标准 """

        # 日有效小时数据
        self.dict_stat_standard['min_num_daily'] = self.spinbox_daily.value()

        # 月有效日数据(非2月)
        self.dict_stat_standard['min_num_monthly'] = self.spinbox_monthly.value()

        # 季节有效日数据
        self.dict_stat_standard['min_num_seasonal'] = self.spinbox_seasonal.value()

        # 年有效日数据
        self.dict_stat_standard['min_num_annual'] = self.spinbox_annual.value()

        # 日最大8小时均值-MDA8窗口大小
        self.dict_stat_standard['min_num_moving_avg'] = self.spinbox_mda8_moving_avg.value()

        # 日最大8小时均值-MDA8有效数据个数
        self.dict_stat_standard['min_num_8h'] = self.spinbox_mda8.value()

    def init_site_type(self):
        """ 初始化站点类型选择控件 """

        # 水平layout
        hlayout_ = QHBoxLayout()

        # label
        label_ = QLabel('站点类型:')

        # ComboBox
        self.comboBox_site_type = QComboBox()
        self.comboBox_site_type.setEnabled(False)

        # 空间占据原则：水平扩展，垂直默认
        self.comboBox_site_type.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        # 添加元素：站点、市、省
        self.comboBox_site_type.addItems(['urban', 'background', 'both'])

        # slot-选择变化时触发
        # self.comboBox_assessment_standard.currentIndexChanged.connect(self.slot_quantile_visible)

        # 添加进水平layout
        hlayout_.addWidget(label_)
        hlayout_.addWidget(self.comboBox_site_type)

        # 添加至vlayout_option
        self.vlayout_option.addLayout(hlayout_)

    def menu_recover_default_dt_range(self):
        """ 恢复默认的数据库时间范围 """

        self.daterange_start.setDate(QDate(*self.dt_min.timetuple()[:3]))
        self.daterange_end.setDate(QDate(*self.dt_max.timetuple()[:3]))

    def menu_recover_default_stat_standard(self):
        """ 恢复默认数据有效性标准 """

        self.spinbox_daily.setValue(cfg.dict_stat_standard['min_num_daily'])
        self.spinbox_monthly.setValue(cfg.dict_stat_standard['min_num_monthly'])
        self.spinbox_seasonal.setValue(cfg.dict_stat_standard['min_num_seasonal'])
        self.spinbox_annual.setValue(cfg.dict_stat_standard['min_num_annual'])
        self.spinbox_mda8.setValue(cfg.dict_stat_standard['min_num_8h'])
        self.spinbox_mda8_moving_avg.setValue(cfg.dict_stat_standard['min_num_moving_avg'])

    def init_statistical_validity(self):
        """ 初始化数据有效性要求输入控件 """

        ''' 数据有效性参数 '''
        self.groupBox_mean = QGroupBox('数据有效性要求')
        self.formLayout_valid_num = QFormLayout(self.groupBox_mean)
        self.formLayout_valid_num.setLabelAlignment(Qt.AlignmentFlag.AlignRight)

        # 日有效小时数据
        self.label_daily = QLabel('日有效小时数据')
        # self.spinbox_daily = QLineEdit('20')
        self.spinbox_daily = QSpinBox(value=cfg.dict_stat_standard['min_num_daily'])
        self.spinbox_daily.setRange(1, 24)

        # 月有效日数据(非2月)
        self.label_monthly = QLabel('月有效日数据')
        # self.spinbox_monthly = QLineEdit('27')
        self.spinbox_monthly = QSpinBox(value=cfg.dict_stat_standard['min_num_monthly'])
        self.spinbox_monthly.setRange(1, 31)

        # 季节有效日数据
        self.label_seasonal = QLabel('季有效日数据')
        # self.spinbox_seasonal = QLineEdit('72')
        self.spinbox_seasonal = QSpinBox(value=cfg.dict_stat_standard['min_num_seasonal'])
        self.spinbox_seasonal.setRange(1, 90)

        # 年有效日数据
        self.label_annual = QLabel('年有效日数据')
        # self.spinbox_year = QLineEdit('324')
        self.spinbox_annual = QSpinBox()
        self.spinbox_annual.setRange(1, 366)
        self.spinbox_annual.setValue(cfg.dict_stat_standard['min_num_annual'])

        # 日最大8小时均值
        self.label_mda8 = QLabel('MDA8有效数据')
        self.label_mda8_moving_avg = QLabel('MDA8窗口大小')
        # self.spinbox_mda8 = QLineEdit('14')
        self.spinbox_mda8 = QSpinBox(value=cfg.dict_stat_standard['min_num_8h'])
        self.spinbox_mda8.setRange(1, 16)

        # self.spinbox_mda8_moving_window = QLineEdit('6')
        self.spinbox_mda8_moving_avg = QSpinBox(value=cfg.dict_stat_standard['min_num_moving_avg'])
        self.spinbox_mda8_moving_avg.setRange(1, 8)

        self.formLayout_valid_num.addRow(self.label_daily, self.spinbox_daily)
        self.formLayout_valid_num.addRow(self.label_monthly, self.spinbox_monthly)
        # self.formLayout_valid_num.addRow(self.label_monthly_Feb, self.line_edit_monthly_Feb)
        self.formLayout_valid_num.addRow(self.label_seasonal, self.spinbox_seasonal)
        self.formLayout_valid_num.addRow(self.label_annual, self.spinbox_annual)
        self.formLayout_valid_num.addRow(self.label_mda8, self.spinbox_mda8)
        self.formLayout_valid_num.addRow(self.label_mda8_moving_avg, self.spinbox_mda8_moving_avg)

        # 添加至vlayout_option
        self.vlayout_option.addWidget(self.groupBox_mean)

    # def slot_quantile_visible(self):
    #     """ 当浓度评价标准变化时触发 """

    #     if self.comboBox_assessment_standard.currentText() == '均值':
    #         self.lineEdit_quantile.setVisible(False)

    #     elif self.comboBox_assessment_standard.currentText() == '分位数':
    #         self.lineEdit_quantile.setVisible(True)

    #     else:
    #         pass

    def init_o3_mda8(self):
        """ 初始化O3_MDA8选择控件 """

        # 水平layout
        hlayout_ = QHBoxLayout()

        # label
        label_ = QLabel('计算MDA8-O<sub>3</sub>:')

        # ComboBox
        self.comboBox_o3_mda8 = QComboBox()

        # 空间占据原则：水平扩展，垂直默认
        self.comboBox_o3_mda8.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        # 添加元素：站点、市、省
        self.comboBox_o3_mda8.addItems(['是', '否'])

        # 功能提示
        self.comboBox_o3_mda8.setToolTip('当时间分辨率为日、月、季、年时，O3的日均值是否使用MDA8-O₃代替。')

        # slot-选择变化时触发
        # self.comboBox_.currentIndexChanged.connect(self.slot_region_level)

        # 添加进水平layout
        hlayout_.addWidget(label_)
        hlayout_.addWidget(self.comboBox_o3_mda8)

        # 添加至vlayout_option
        self.vlayout_option.addLayout(hlayout_)

    def context_menu_add2candidate_province(self, pos):
        """ 右键菜单：添加所选至待导出-省级行政区 """

        # 判断导出区域级别是否正确
        if self.region_level != 'province':
            return

        # 获取所选的元素
        items = self.listWidget_province.selectedItems()

        if not items:
            return

        # 初始化右键菜单
        context_menu = QMenu()

        # 菜单选项-添加至待导出
        action_add = context_menu.addAction('添加至待导出')

        # slot-点击时触发
        action_add.triggered.connect(lambda: self.add_candidate(items))

        # 根据区域级别确定右键菜单弹出位置
        context_menu.exec(self.listWidget_province.mapToGlobal(pos))

    def context_menu_add2candidate_city(self, pos):
        """ 右键菜单：添加所选至待导出-地市行政区 """

        # 判断导出区域级别是否正确
        if self.region_level != 'city':
            return

        # 获取所选的元素
        items = self.listWidget_city.selectedItems()

        if not items:
            return

        # 初始化右键菜单
        context_menu = QMenu()

        # 菜单选项-添加至待导出
        action_add = context_menu.addAction('添加至待导出')

        # slot-点击时触发
        action_add.triggered.connect(lambda: self.add_candidate(items))

        # 根据区域级别确定右键菜单弹出位置
        context_menu.exec(self.listWidget_city.mapToGlobal(pos))

    def context_menu_add2candidate_site(self, pos):
        """ 右键菜单：添加所选至待导出-站点 """

        # 判断导出区域级别是否正确
        if self.region_level != 'site':
            return

        # 获取所选的元素
        items = self.listWidget_site.selectedItems()

        if not items:
            return

        # 初始化右键菜单
        context_menu = QMenu()

        # 菜单选项-添加至待导出
        action_add = context_menu.addAction('添加至待导出')

        # slot-点击时触发
        action_add.triggered.connect(lambda: self.add_candidate(items))

        # 根据区域级别确定右键菜单弹出位置
        context_menu.exec(self.listWidget_site.mapToGlobal(pos))

    def context_menu_delete_candidate(self, pos):
        """ 右键菜单：删除待导出候选项 """

        # 初始化右键菜单
        context_menu = QMenu()

        # 获取所选的元素
        items = self.listWidget_export.selectedItems()

        if items:
            # 菜单选项-删除所选
            action_del = context_menu.addAction('删除所选')

            # slot-点击时触发-删除所选
            action_del.triggered.connect(self.del_candidate)

        # 菜单选项-清空
        action_clear = context_menu.addAction('清空')

        # slot-点击时触发-清空
        action_clear.triggered.connect(self.listWidget_export.clear)

        # 根据区域级别确定右键菜单弹出位置
        context_menu.exec(self.listWidget_export.mapToGlobal(pos))

    def del_candidate(self):
        """ 删除所选待导出候选项 """

        # 所选待导出的索引-逆序
        current_export = sorted([i.row() for i in self.listWidget_export.selectedIndexes()], reverse=True)

        # 挨个删除
        for r in current_export:
            self.listWidget_export.takeItem(r)

    def slot_selected_species(self):
        """ 物种选择变化时触发 """

        self.list_selected_species = [i.text() for i in self.listWidget_species.selectedItems()]
        # print(self.slot_selected_species)

    def slot_list2export(self):
        """ 导出列表变化时触发 """

        self.list2export = [self.listWidget_export.item(i).text() for i in range(self.listWidget_export.count())]
        print(self.list2export)

    def add_candidate(self, items_):
        """ 将元素添加至待导出列表 """

        # 待添加元素
        list_candidate = [i.text() for i in items_]

        # 已存在元素
        existing_items = [self.listWidget_export.item(i).text() for i in range(self.listWidget_export.count())]

        # 待添加元素去重
        list_candidate = [i for i in list_candidate if i not in existing_items]

        # 添加至待导出列表
        self.listWidget_export.addItems(list_candidate)

        # 更新计数
        self.label_export.setText('待导出(' + str(len(self.listWidget_export)) + ')')

    def slot_region_level(self):
        """ 选择导出区域级别时触发, 更新self.region_level参数 """

        # 清空待导出列表
        self.listWidget_export.clear()

        # 更新参数
        if self.comboBox_region_level.currentText() == '省':
            self.region_level = 'province'
            self.comboBox_site_type.setEnabled(True)

        elif self.comboBox_region_level.currentText() == '市':
            self.region_level = 'city'
            self.comboBox_site_type.setEnabled(True)

        elif self.comboBox_region_level.currentText() == '站点':
            self.region_level = 'site'
            self.comboBox_site_type.setEnabled(False)

        else:
            raise ValueError('区域级别错误！')

    def slot_itemSelectionChanged_province(self):
        """ 省级行政区选择改变时时触发 """

        # 断开地市级信号槽
        self.listWidget_city.disconnect()

        # 清空地市级列表和站点列表
        self.listWidget_city.clear()
        self.listWidget_site.clear()

        # 当前选中的省级行政区列表
        list_province = [i.text() for i in self.listWidget_province.selectedItems()]

        if list_province:
            # 选中的省份包含的地级市列表
            list_city = np.hstack([self.si.dict_province2city[i] for i in list_province])

            # 更新界面进地市级列表
            self.listWidget_city.addItems(list_city)

        # 重新连接信号槽
        self.listWidget_city.itemSelectionChanged.connect(self.slot_itemSelectionChanged_city)
        self.listWidget_city.customContextMenuRequested.connect(self.context_menu_add2candidate_city)

        # 更新计数
        self.label_city.setText('地市(' + str(len(self.listWidget_city)) + ')')

    def slot_itemSelectionChanged_city(self):
        """ 地市级行政区选择改变时触发 """

        # 清空站点列表
        self.listWidget_site.clear()

        # 当前选中的地市级行政区列表
        list_city = [i.text() for i in self.listWidget_city.selectedItems()]

        # 选中的地市包含的站点列表
        list_site = np.hstack([self.si.dict_city2code[i] for i in list_city])

        # 重新组合站点框中显示的内容：站点名[站点代码]{纬度,经度}{是否为对照点}
        # dict_backgroud = {True: '对照点', False: ''}
        # list_site = [self.si.dict_code2name[i] + ' [' + i + ']' + ' {' + ','.join([str(gkd.round_accurately(k, 3)) for k in self.dict_code2coord[i]]) + '} ' + dict_backgroud[self.si.dict_code2background[i]] for i in list_site]

        list_site = gkd.format_list2export(sites_=list_site, site_info=self.si)

        # 更新界面站点列表
        self.listWidget_site.addItems(list_site)

        # 更新计数
        self.label_site.setText('站点(' + str(len(self.listWidget_site)) + ')')

    def menu_region_select(self):
        """ 当菜单栏-重点区域选项点击时触发 """

        # 导出区域级别ComboBox设置为'市'
        if self.comboBox_region_level.currentIndex() != 1:
            self.comboBox_region_level.setCurrentIndex(1)
        else:
            # 清空待导出列表
            self.listWidget_export.clear()

        # 获取发送信号的对象
        action = self.sender().text()

        if action == '全国':
            self.listWidget_export.addItems(self.si.list_city_china)

        elif action == '74个重点城市':
            self.listWidget_export.addItems(self.si.list_city_74)

        elif action == '"2+26"城市':
            self.listWidget_export.addItems(self.si.list_city_2p26)

        elif action == '京津冀':
            self.listWidget_export.addItems(self.si.list_city_jjj)

        elif action == '珠三角':
            self.listWidget_export.addItems(self.si.list_city_prd)

        elif action == '长三角':
            self.listWidget_export.addItems(self.si.list_city_yrd)

        elif action == '四川盆地':
            self.listWidget_export.addItems(self.si.list_city_scb)

        elif action == '汾渭平原':
            self.listWidget_export.addItems(self.si.list_city_fwp)

        else:
            pass

        # 更新计数
        self.label_export.setText('待导出(' + str(len(self.listWidget_export)) + ')')

    def menu_shortcut(self, item: dict):
        """ 快捷操作 """

        # 日期范围
        if 'dt_range' in item.keys():
            self.set_date_range(pd.to_datetime(item['dt_range'][0]), pd.to_datetime(item['dt_range'][1]))
        
        # 数据有效性要求
        if 'dict_stat_standard' in item.keys():
            self.spinbox_daily.setValue(item['dict_stat_standard']['min_num_daily'])
            self.spinbox_monthly.setValue(item['dict_stat_standard']['min_num_monthly'])
            self.spinbox_seasonal.setValue(item['dict_stat_standard']['min_num_seasonal'])
            self.spinbox_annual.setValue(item['dict_stat_standard']['min_num_annual'])
            self.spinbox_mda8.setValue(item['dict_stat_standard']['min_num_8h'])
            self.spinbox_mda8_moving_avg.setValue(item['dict_stat_standard']['min_num_moving_avg'])
        
        # 站点类型
        if 'site_type' in item.keys():
            self.comboBox_site_type.setCurrentText(item['site_type'])
        
        # 时间分辨率
        if 'time_resolution' in item.keys():
            self.comboBox_tr.setCurrentText(item['time_resolution'])

        # 导出区域级别
        if 'region_level' in item.keys():
            self.comboBox_region_level.setCurrentText(item['region_level'])

        # 浓度评价标准
        if 'stat_standard' in item.keys():
            self.comboBox_stat_standard.setCurrentText(item['stat_standard'])
        
        # 计算MDA8-O3
        if 'o3-mda8' in item.keys():
            self.comboBox_o3_mda8.setCurrentText(item['o3-mda8'])

    def set_date_range(self, start: pd.Timestamp, end: pd.Timestamp):
        """ 设置日期范围 """

        # 设置开始日期
        self.daterange_start.setDate(QDate(*start.timetuple()[:3]))
        
        # 设置结束日期
        self.daterange_end.setDate(QDate(*end.timetuple()[:3]))


    def get_datetime_range(self):
        """ 获取开始时间和结束时间 """

        # 判断哪些物种的数据库存在
        self.list_species_existing = [s for s in cfg.list_species if os.path.exists(os.path.join(cfg.dir_hdf5, s + '.h5'))]

        if self.list_species_existing:
            # 读取数据库时间范围
            self.dt_min, self.dt_max = gkd.get_datetime_range(self.list_species_existing[0])
        else:
            self.dt_min, self.dt_max = None, None

    def __update_progress_bar(self, dict_):
        """ 更新进度条 
            dict_: 字典, 必须包含'text'和'value'键, 它们的value分别为str和int """

        # 更新状态文字
        self.progress.setLabelText(dict_['text'])

        # 更新进度值
        if dict_['value'] is None:
            pass

        elif dict_['value'] != 1000:
            self.progress.setValue(int(dict_['value']))

        else:
            self.progress.close()

    def after_save_data(self, dict_):
        """ 在保存数据完成后询问是否打开文件所在路径 """

        # 更新进度条
        self.__update_progress_bar(dict_)

        # 待数据保存完成后继续操作
        if self.progress.isVisible():
            return

        # 询问是否打开目标文件夹
        MsgBoxOpenFolder(path_file=dict_['path_xlsx'])

    def after_csv2h5(self, dict_: dict):
        """ 后处理-在menu_csv2h5函数之后运行 """

        # 更新进度条
        self.__update_progress_bar(dict_)

        # 如果生成数据库操作已经完成
        if self.progress.isVisible():
            return

        # 获取数据库最新日期
        self.get_datetime_range()

        # 更新显示日期
        self.menu_recover_default_dt_range()

        # 更新物种列表
        self.listWidget_species.clear()
        self.listWidget_species.addItems(self.list_species_existing)

        # 更新状态栏
        self.label_status_left.setText('  数据库：%s─%s  ▏ ' % (self.dt_min.strftime('%Y.%m.%d'), self.dt_max.strftime('%Y.%m.%d')))

    def after_aqi_bar(self, dict_: dict):
        """ 在计算完污染程度并作图后询问是否保存数据 """

        # 更新进度条
        self.__update_progress_bar(dict_)

        # 待作图完成后继续操作
        if self.progress.isVisible():
            return

        # 询问是否保存数据
        filename_xlsx = self.region_level + ',' + self.daterange_start.date().toString('yyyyMMdd') + \
                        '~' + self.daterange_end.date().toString('yyyyMMdd') + ',' + self.time_resolution_aqi_bar + \
                        ',AQI_Bar.xlsx'

        # 保存数据中
        self.save_data_thread(dict_data=dict_['data'], filename=filename_xlsx)

    def after_primary_pollutant(self, dict_: dict):
        """ 在计算完首要污染物并作图后询问是否保存数据 """

        # 更新进度条
        self.__update_progress_bar(dict_)

        # 待作图完成后继续操作
        if self.progress.isVisible():
            return

        # 询问是否保存数据
        filename_xlsx = self.region_level + ',' + self.daterange_start.date().toString('yyyyMMdd') + \
                        '~' + self.daterange_end.date().toString('yyyyMMdd') + ',MS,PrimaryPollutant.xlsx'

        # 保存数据中
        self.save_data_thread(dict_data=dict_['data'], filename=filename_xlsx)

    def after_exceed_standard(self, dict_: dict):
        """ 在超标天数作图完成后询问是否保存数据 """

        # 更新进度条
        self.__update_progress_bar(dict_)

        # 待作图完成后继续操作
        if self.progress.isVisible():
            return

        # 保存数据中
        self.save_data_thread(dict_data=dict_['data'], filename=dict_['filename_xlsx'])

    def after_load_data(self, dict_):
        """ 从LoadData中获取进度值, 然后更新到进度条 """

        # 更新进度条文字和值
        self.__update_progress_bar(dict_)

        # 待数据读取完成后继续操作
        if self.progress.isVisible():
            return

        # 根据提取数据前的操作进行处理
        if self.current_operation == 'btn_提取数据':

            # 数据保存默认文件名
            path_xlsx = self.region_level + ',' + self.daterange_start.date().toString('yyyyMMdd') + \
                        '~' + self.daterange_end.date().toString('yyyyMMdd') + ',' + self.time_resolution + \
                        ',' + self.comboBox_stat_standard.currentText() + '.xlsx'

            self.save_data_thread(dict_data=self.thread_get_data.data, filename=path_xlsx)

        elif self.current_operation == 'plot_annual':
            self.after_load_data_bar_annual()

        elif self.current_operation == 'plot_annual_by_species':
            plot.plot_annual_by_species(self.thread_get_data.data, window_title='Plot Annual by Species')

        elif self.current_operation == 'plot_annual_by_regions':
            plot.plot_annual_by_region(self.thread_get_data.data, window_title='Plot Annual by Regions')

        elif self.current_operation == 'plot_primary_pollutant':
            self.after_load_data_primary_pollutant()

        elif self.current_operation == 'plot_exceed_standard':
            self.after_load_data_exceed_standard()

        elif self.current_operation == 'plot_aqi_calendar':
            self.after_load_data_aqi_calendar()

        elif self.current_operation == 'plot_aqi_bar_by_city':
            self.after_load_data_aqi_bar_by_city()

        elif self.current_operation == 'plot_ts_by_species':
            self.after_load_data_time_series_by(by_='species')

        elif self.current_operation == 'plot_ts_by_regions':
            self.after_load_data_time_series_by(by_='regions')

        elif self.current_operation == 'plot_geo_by_species':

            # 默认显示精度
            precision = 0

            # 根据物种选择合适的colormap范围
            if self.species_geo == 'PM2.5':
                piecies = cfg.pieces_pm2p5
            elif self.species_geo == 'PM10':
                piecies = cfg.pieces_pm10
            elif self.species_geo == 'O3-MDA8-Mean':
                piecies = cfg.pieces_o3
            elif self.species_geo == 'O3-MDA8-90th':
                piecies = cfg.pieces_o3_90th
            elif self.species_geo == 'NO2':
                piecies = cfg.pieces_no2
            elif self.species_geo == 'SO2':
                piecies = cfg.pieces_so2
            elif self.species_geo == 'CO-Mean':
                piecies = cfg.pieces_co
                precision = 1
            elif self.species_geo == 'CO-95th':
                piecies = cfg.pieces_co_95th
                precision = 1
            else:
                raise ValueError('self.species_geo_bar')

            # html默认保存路径
            self.path_save = os.path.join(cfg.dir_default_save, 'Geo_China_' + self.species_geo + ',' + self.time_resolution + '.html')

            # html保存路径
            path_html, _ = QFileDialog.getSaveFileName(self,
                                                       caption='作图文件保存至...',
                                                       directory=self.path_save,
                                                       filter='html 文件(*.html)',
                                                       )
            if path_html:
                # 当前选中的物种
                species = self.list_selected_species[0]

                # 提取数据
                df_data = self.thread_get_data.data[species].T

                # 更改columns
                df_data.columns = [gkd.format_dt(dt, self.time_resolution) for dt in df_data.columns]

                # 经纬度数据
                df_coords = pd.DataFrame(data=self.si.dict_code2coordinate, index=['lat', 'lon']).T.loc[:, ['lon', 'lat']]

                # 作图
                plot_html.geo_china(data_=df_data, path_html=path_html, coords_=df_coords, pieces_=piecies, color_bg=cfg.color_geo_background, precision=precision)

    def save_data_thread(self, dict_data: dict, filename: str):
        """ 保存数据 """

        result = YesOrNo(window_title='询问', tips='是否保存数据?').exec()

        if result == "否":
            return

        # 保存路径
        self.path_save = os.path.join(cfg.dir_default_save, filename)

        # 获取xlsx数据保存路径
        path_save_xlsx, _ = QFileDialog.getSaveFileName(self,
                                                        caption='保存数据',
                                                        directory=self.path_save,
                                                        filter='Excel 文件(*.xlsx)',
                                                        )

        if not path_save_xlsx:
            return

        # 重新赋值
        self.path_save = path_save_xlsx

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        # 初始化QThread
        self.thread_save_data = SaveData(data=dict_data,
                                         path_save=path_save_xlsx,
                                         time_resolution=self.time_resolution,
                                         )

        # 读取进度返回
        self.thread_save_data.signal_progress.connect(self.after_save_data)

        # 错误信息返回
        self.thread_save_data.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_save_data.terminate())

        self.thread_save_data.start()

    def show_progress_bar(self, fuzzy=True):
        """ 数据载入进度条设置 """

        self.progress = QProgressDialog()
        # self.progress = QProgressBar()

        # 进度条对话框窗口标题
        self.progress.setWindowTitle(self.windowTitle())

        # 进度条对话框窗口图标
        self.progress.setWindowIcon(QIcon(cfg.path_logo))

        # 进度条对话框宽度
        self.progress.setMinimumWidth(400)

        # 进度条上方文字
        self.progress.setLabelText("数据读取进度")

        # 取消取消按钮
        self.progress.setCancelButton(None)
        self.progress.setMinimumDuration(100)

        # 进度条对话框显示时，其他窗口无法点击
        self.progress.setWindowModality(Qt.WindowModality.ApplicationModal)

        # 进度条范围
        if fuzzy:
            self.progress.setRange(0, 0)
        else:
            self.progress.setRange(0, 100)

        # 自动关闭/复位：关
        self.progress.setAutoClose(False)
        self.progress.setAutoReset(False)

        # 显示进度条对话框
        self.progress.show()

    def error_handler(self, etype, value, tb):
        """ 错误处理，用于捕获程序运行过程中的错误，并弹出错误警告对话框 """

        # 获取错误信息
        error_msg = ''.join(traceback.format_exception(etype, value, tb))

        # 显示错误信息
        self.show_error(error_msg)

    def show_error(self, error=''):
        """ 显示错误信息窗口 """

        # 关闭进度条
        if self.progress is not None:
            self.progress.close()

        QMessageBox.critical(self, '错误信息', error, QMessageBox.StandardButton.Ok)  # 显示错误信息

    def load_data(self, dict_percentile=None):
        """ 点击运行按钮触发 """

        # 选中的物种
        if not self.list_selected_species:
            return

        # 待导出省/市/站点列表
        self.list2export = [self.listWidget_export.item(i).text() for i in range(self.listWidget_export.count())]

        if not self.list2export:
            return

        # 浓度评价标准
        if dict_percentile is not None:
            self.run(dict_percentile)

        elif self.comboBox_stat_standard.currentText() == '百分位数':

            # 初始化窗口
            self.w = PercentileInput()

            # 数据传回
            self.w.signal_values.connect(self.run)

            # 弹出窗口
            self.w.show()

        elif self.comboBox_stat_standard.currentText() == '均值':

            # 默认列表
            dict_values = dict()

            self.run(dict_values)

        else:
            pass

    def run(self, dict_percentile):

        # 设定的日期时间
        date_start = self.daterange_start.date().toString('yyyy-MM-dd')  # 时间范围-开始
        date_end = self.daterange_end.date().toString('yyyy-MM-dd')  # 时间范围-结束

        # 统计有效性标准
        self.get_stat_standard()

        # O3_MDA8状态
        if self.comboBox_o3_mda8.currentText() == '是':
            o3_mda8 = True
        elif self.comboBox_o3_mda8.currentText() == '否':
            o3_mda8 = False

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        """ 读取数据 """
        self.thread_get_data = gkd.ReadGkd(
            dir_h5=cfg.dir_hdf5,
            speices=self.list_selected_species,
            date_range=[date_start, date_end],
            dict_stat_standard=self.dict_stat_standard,
            dict_percentile=dict_percentile,
            site_info=self.si,
            list2export=self.list2export,
            site_type=self.comboBox_site_type.currentText(),
            time_resolution=self.time_resolution,
            region_level=self.region_level,
            o3_mda8=o3_mda8,
        )

        # 读取进度返回
        self.thread_get_data.signal_progress.connect(self.after_load_data)

        # 错误信息返回
        self.thread_get_data.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_get_data.terminate())

        # 启动进程
        self.thread_get_data.start()

    def slot_click_load_data(self):
        """ 点击提取数据按钮触发 """

        # 当前操作
        self.current_operation = 'btn_提取数据'

        # 载入数据
        self.load_data()

    def menu_plot_annual(self):
        """ 年均值趋势-基于bokeh库 """

        # 当前操作
        self.current_operation = 'plot_annual'

        # 更改时间分辨率为年
        self.comboBox_tr.setCurrentIndex(4)

        # 载入数据
        self.load_data()

    def menu_plot_annual_by_species(self):
        """ 年均值趋势, 子图对应不同物种 """

        # 当前操作
        self.current_operation = 'plot_annual_by_species'

        # 更改时间分辨率
        self.comboBox_tr.setCurrentIndex(4)

        # 载入数据
        self.load_data()

    def menu_plot_annual_by_regions(self):
        """ 年均值趋势, 子图对应不同省市站点 """

        # 当前操作
        self.current_operation = 'plot_annual_by_regions'

        # 更改时间分辨率为年
        self.comboBox_tr.setCurrentIndex(4)

        # 载入数据
        self.load_data()

    def menu_plot_primary_pollutant(self):
        """ 首要污染物占比变化趋势, 点击菜单栏触发 """

        # 当前操作
        self.current_operation = 'plot_primary_pollutant'

        # 更改时间分辨率为日
        self.comboBox_tr.setCurrentIndex(1)

        # 选取除AQI外的全部物种
        self.listWidget_species.selectAll()  # 先选中所有元素
        last_item = self.listWidget_species.item(self.listWidget_species.count() - 1)  # 获取最后一个元素
        last_item.setSelected(False)  # 取消选中最后一个元素

        # 判断当前导出区域是否为城市
        if self.region_level != 'city':
            # raise ValueError('导出区域需要是城市，统计所有城市组成的区域的首要污染物占比')
            self.show_error(error='导出区域级别必须是城市，统计城市组成的区域的首要污染物占比！')
            return

        # 载入数据
        self.load_data()

    def menu_plot_aqi_calendar(self):
        """ AQI日历热力图 """

        # 当前操作
        self.current_operation = 'plot_aqi_calendar'

        # 更改时间分辨率为日
        self.comboBox_tr.setCurrentIndex(1)

        # 选取AQI
        self.listWidget_species.clearSelection()
        last_item = self.listWidget_species.item(self.listWidget_species.count() - 1)  # 获取最后一个元素，即AQI
        last_item.setSelected(True)

        # 判断当前导出区域是否为城市
        if self.region_level != 'city':
            # raise ValueError('导出区域需要是城市，统计所有城市组成的区域的首要污染物占比')
            self.show_error(error='导出区域级别必须是城市，统计城市组成的区域的AQI！')
            return

        # 载入数据
        self.load_data()

    def menu_plot_aqi_bar(self, time_resolution):
        """ AQI分城市统计 """

        # 全局变量
        self.time_resolution_aqi_bar = time_resolution

        # 当前操作
        self.current_operation = 'plot_aqi_bar_by_city'

        # 更改时间分辨率为日
        self.comboBox_tr.setCurrentIndex(1)

        # 选取AQI
        self.listWidget_species.clearSelection()
        last_item = self.listWidget_species.item(self.listWidget_species.count() - 1)  # 获取最后一个元素，即AQI
        last_item.setSelected(True)

        # 判断当前导出区域是否为城市
        if self.region_level != 'city':
            # raise ValueError('导出区域需要是城市，统计所有城市组成的区域的首要污染物占比')
            self.show_error(error='导出区域级别必须是城市，统计各城市的AQI！')
            return

        # 载入数据
        self.load_data()

    def menu_plot_exceed_standard(self, species: str):
        """ PM2.5和O3的超标天数统计 """

        # 当前操作
        self.current_operation = 'plot_exceed_standard'

        # 超标选项
        self.exceed_standard_species = species

        # 更改时间分辨率为日
        self.comboBox_tr.setCurrentIndex(1)

        # 取消全选
        self.listWidget_species.clearSelection()

        # 选中物种
        if species == 'PM2.5+O3':
            species = ['PM2.5', 'O3']

            # 计算MDA8-O3设置为“是”
            self.comboBox_o3_mda8.setCurrentIndex(0)

        elif species == 'PM2.5':
            species = ['PM2.5']
        elif species == 'O3':
            species = ['O3']
            
            # 计算MDA8-O3设置为“是”
            self.comboBox_o3_mda8.setCurrentIndex(0)
        
        for s in species:
            item = self.listWidget_species.findItems(s, Qt.MatchFlag.MatchExactly)[0]
            item.setSelected(True)

        # 判断当前导出区域是否为城市
        if self.region_level != 'city':
            # raise ValueError('导出区域需要是城市，统计所有城市组成的区域的首要污染物占比')
            self.show_error(error='导出区域级别必须是城市，统计城市组成的区域的首要污染物占比！')
            return

        # 载入数据
        self.load_data()

    def menu_plot_geo_by_species(self, species_geo_bar, time_resolution):
        """ 全国各站点作图-Geo """

        # 当前操作
        self.current_operation = 'plot_geo_by_species'

        # 全局变量
        self.species_geo = species_geo_bar

        # 更改时间分辨率
        if time_resolution == 'YS':
            self.comboBox_tr.setCurrentIndex(4)
        elif time_resolution == 'QS-MAR':
            self.comboBox_tr.setCurrentIndex(3)
        elif time_resolution == 'MS':
            self.comboBox_tr.setCurrentIndex(2)

        # 更改导出区域级别为site
        self.comboBox_region_level.setCurrentIndex(0)

        # 清除选择
        self.listWidget_export.clear()
        # 添加全国站点
        self.listWidget_export.addItems(gkd.format_list2export(sites_=self.si.list_code_china, site_info=self.si))

        # 更新计数
        self.label_export.setText('待导出(' + str(self.listWidget_export.count()) + ')')

        # 取消全选
        self.listWidget_species.clearSelection()
        
        # 计算MDA8-O3设置为“是”
        if species_geo_bar in ['O3-MDA8-Mean', 'O3-MDA8-90th']:
            self.comboBox_o3_mda8.setCurrentIndex(0)

        # 如果是'O3-MDA8-90th'这种形式的species，则需要提取其中的'O3'
        if '-' in species_geo_bar:
            species_geo_bar = species_geo_bar.split('-')[0]

        # 选取物种species
        item = self.listWidget_species.findItems(species_geo_bar, Qt.MatchFlag.MatchExactly)[0]
        item.setSelected(True)

        # 百分位数设置
        if self.species_geo == 'O3-MDA8-90th':
            dict_percentile = {'O3': 90}
        elif self.species_geo == 'CO-95th':
            dict_percentile = {'CO': 95}
        else:
            dict_percentile = None

        # 载入数据
        self.load_data(dict_percentile)

    def menu_plot_ts_by_species(self):
        """ 时间序列作图, 子图对应不同物种 """

        # 当前操作
        self.current_operation = 'plot_ts_by_species'

        # 载入数据
        self.load_data()

    def menu_plot_ts_by_regions(self):
        """ 时间序列作图, 子图对应不同省市站点 """

        # 当前操作
        self.current_operation = 'plot_ts_by_regions'

        # 载入数据
        self.load_data()

    def after_load_data_aqi_calendar(self):
        """ AQI日历热力图, 数据载入完成后触发 """

        # html默认保存路径
        self.path_save = os.path.join(cfg.dir_default_save, 'AQI_Calendar_HeatMap.html')

        # html保存路径
        path_html, _ = QFileDialog.getSaveFileName(self,
                                                   caption='作图文件保存至...',
                                                   directory=self.path_save,
                                                   filter='html 文件(*.html)',
                                                   )
        if path_html:
            # 计算AQI均值
            series_aqi = self.thread_get_data.data['AQI'].mean(axis=1)

            # 取整
            series_aqi = series_aqi.round(0)

            # 作图
            plot_html.calendar_heatmap(data_=series_aqi, path_html=path_html)

    def after_load_data_aqi_bar_by_city(self):
        """ AQI分城市统计, 数据载入完成后触发 """

        # html默认保存路径
        self.path_save = os.path.join(cfg.dir_default_save, 'AQI_Bar_City,' + self.time_resolution_aqi_bar + '.html')

        # html保存路径
        path_html, _ = QFileDialog.getSaveFileName(self,
                                                   caption='作图文件保存至...',
                                                   directory=self.path_save,
                                                   filter='html 文件(*.html)',
                                                   )

        if not path_html:
            return

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        # 开启新进程计算AQI污染级别并作图，防止界面卡顿
        self.thread_plot_aqi_bar = gkd.AQI2PollutionLevel(data_=self.thread_get_data.data['AQI'],
                                                          path_html=path_html,
                                                          time_resolution=self.time_resolution_aqi_bar,
                                                          )

        self.thread_plot_aqi_bar.signal_progress.connect(self.after_aqi_bar)
        self.thread_plot_aqi_bar.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_plot_aqi_bar.terminate())

        self.thread_plot_aqi_bar.start()

    def after_load_data_primary_pollutant(self):
        """ 首要污染物占比变化趋势, 数据载入完成后触发 """

        # html默认保存路径
        self.path_save = os.path.join(cfg.dir_default_save, 'Primary Pollutant_City.html')

        # html保存路径
        path_html, _ = QFileDialog.getSaveFileName(self,
                                                   caption='作图文件保存至...',
                                                   directory=self.path_save,
                                                   filter='html 文件(*.html)',
                                                   )
        if not path_html:
            return

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        # 开启新进程计算首要污染物并作图，防止界面卡顿
        self.thread_plot_primary_pollutant = gkd.CalPrimaryPolltant(data_=self.thread_get_data.data,
                                                                    path_html=path_html,
                                                                    time_resolution='MS',
                                                                    )
        self.thread_plot_primary_pollutant.signal_progress.connect(self.after_primary_pollutant)
        self.thread_plot_primary_pollutant.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_plot_primary_pollutant.terminate())

        self.thread_plot_primary_pollutant.start()


    def after_load_data_exceed_standard(self):
        """ 超标天数统计, 数据载入完成后触发 """

        # html默认保存路径
        if self.exceed_standard_species == 'PM2.5':
            self.path_save = os.path.join(cfg.dir_default_save, '超标天数统计,PM2.5日均值,分城市.html')
            # species, threshold = 'PM2.5日均值', (35, 75)

        elif self.exceed_standard_species == 'O3':
            self.path_save = os.path.join(cfg.dir_default_save, '超标天数统计,O3-MDA8,分城市.html')
            # species, threshold = 'O3-MDA8', (100, 160)

        elif self.exceed_standard_species == 'PM2.5+O3':
            self.path_save = os.path.join(cfg.dir_default_save, '超标天数统计,PM2.5日均值&O3-MDA8,分城市.html')
            # species, threshold = 'O3-MDA8', (100, 160)

        # 作图数据保存路径
        filename_xlsx = self.region_level + ',' + self.daterange_start.date().toString('yyyyMMdd') + \
                        '~' + self.daterange_end.date().toString('yyyyMMdd') + ',MS,超标天数统计,' + \
                            self.exceed_standard_species + '.xlsx'

        # html保存路径
        path_html, _ = QFileDialog.getSaveFileName(self,
                                                   caption='作图文件保存至...',
                                                   directory=self.path_save,
                                                   filter='html 文件(*.html)',
                                                   )
        if not path_html:
            return

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        # 开启新进程计算首要污染物并作图，防止界面卡顿
        self.thread_plot_primary_pollutant = gkd.ExceedStandard(data_=self.thread_get_data.data,
                                                                    path_html=path_html,
                                                                    species=self.exceed_standard_species,
                                                                    # threshold=threshold,
                                                                    filename_xlsx=filename_xlsx,
                                                                    )
        
        self.thread_plot_primary_pollutant.signal_progress.connect(self.after_exceed_standard)
        self.thread_plot_primary_pollutant.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_plot_primary_pollutant.terminate())

        self.thread_plot_primary_pollutant.start()


    def after_load_data_bar_annual(self):
        """ 年均值柱状图, 数据载入完成后触发 """

        # 当前的区域级别
        region_level = self.region_level

        # 当前的时间分辨率
        time_resolution = self.time_resolution

        # html默认文件名
        filename_html = 'Bar,' + region_level + ',' + self.daterange_start.date().toString('yyyyMMdd~') + \
                        self.daterange_end.date().toString('yyyyMMdd,') + time_resolution + '.html'

        # html默认保存路径
        self.path_save = os.path.join(cfg.dir_default_save, filename_html)

        # html保存路径
        path_html, _ = QFileDialog.getSaveFileName(self,
                                                   caption='作图文件保存至...',
                                                   directory=self.path_save,
                                                   filter='html 文件(*.html)',
                                                   )
        if not path_html:
            return

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        # 开启新进程计算首要污染物并作图，防止界面卡顿
        self.thread_plot_primary_pollutant = gkd.PlotBar(data_=self.thread_get_data.data,
                                                        path_html=path_html,
                                                        )
        self.thread_plot_primary_pollutant.signal_progress.connect(self.__update_progress_bar)
        self.thread_plot_primary_pollutant.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_plot_primary_pollutant.terminate())

        self.thread_plot_primary_pollutant.start()

    def after_load_data_time_series_by(self, by_='species'):
        """ 时间序列作图, 数据载入完成后触发 """

        # 当前的区域级别
        region_level = self.region_level

        # 当前的时间分辨率
        time_resolution = self.time_resolution

        # html默认文件名
        if by_ == 'species':
            filename_html = 'TS,bySpecies,' + region_level + ',' + self.daterange_start.date().toString('yyyyMMdd~') + \
                        self.daterange_end.date().toString('yyyyMMdd,') + time_resolution + '.html'
        elif by_ == 'regions':
            filename_html = 'TS,byRegions,' + region_level + ',' + self.daterange_start.date().toString('yyyyMMdd~') + \
                        self.daterange_end.date().toString('yyyyMMdd,') + time_resolution + '.html'

        # html默认保存路径
        self.path_save = os.path.join(cfg.dir_default_save, filename_html)

        # html保存路径
        path_html, _ = QFileDialog.getSaveFileName(self,
                                                   caption='作图文件保存至...',
                                                   directory=self.path_save,
                                                   filter='html 文件(*.html)',
                                                   )
        if not path_html:
            return

        # 显示进度条
        self.show_progress_bar(fuzzy=True)

        # 开启新进程计算首要污染物并作图，防止界面卡顿
        self.thread_plot_primary_pollutant = gkd.PlotTS(data_=self.thread_get_data.data,
                                                        path_html=path_html,
                                                        time_resolution=time_resolution,
                                                        by=by_,
                                                        )
        self.thread_plot_primary_pollutant.signal_progress.connect(self.__update_progress_bar)
        self.thread_plot_primary_pollutant.signal_error.connect(self.show_error)

        # 中途关闭进度条时结束进程
        self.progress.canceled.connect(lambda: self.thread_plot_primary_pollutant.terminate())

        self.thread_plot_primary_pollutant.start()


class YesOrNo(QMessageBox):
    """ 对话框, 按钮: YES、NO """

    def __init__(self, window_title, tips):
        super().__init__()

        # 标题栏标题
        self.setWindowTitle(window_title)

        # 标题栏logo
        self.setWindowIcon(QIcon(cfg.path_logo))

        # 提示图标
        self.setIcon(QMessageBox.Icon.Question)

        # 提示文字
        self.setText(tips)

        # 创建按钮
        self.btn_yes = QPushButton('是')
        self.btn_no = QPushButton('否')

        # 存储自定义数据
        self.btn_yes.data = '是'
        self.btn_no.data = '否'

        # 添加按钮
        self.addButton(self.btn_yes, QMessageBox.ButtonRole.AcceptRole)
        self.addButton(self.btn_no, QMessageBox.ButtonRole.RejectRole)
        self.setDefaultButton(self.btn_yes)

    def exec(self):

        super().exec()

        # 返回点击按钮的自定义数据
        return self.clickedButton().data

class MsgBoxOpenFolder(QWidget):
    """ 提示框 | v1.2新增
        在数据保存完成后弹出此提示框，可以选择打开数据保存路径
        
        path_file: str, 要打开的文件夹路径和要选中的文件名

    2023-08-15 v1
    """

    def __init__(self, path_file):
        super().__init__()

        # 待打开的路径和选中的文件
        self.path_file = os.path.abspath(path_file)

        # 调用消息框QMessageBox
        message_box = QMessageBox()

        # 设置消息框的图标
        icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxInformation).pixmap(100, 100)
        message_box.setIconPixmap(icon)

        # 标题栏图标
        message_box.setWindowIcon(QIcon(cfg.path_logo))

        # 标题栏标题
        message_box.setWindowTitle("提示")

        # 提示信息
        message_box.setText("数据保存完成！")

        # 按钮-打开文件夹
        btn_open = message_box.addButton("打开文件夹", QMessageBox.ButtonRole.AcceptRole)
        btn_open.clicked.connect(self.open_folder)

        # 按钮-打开文件夹
        message_box.addButton("退出", QMessageBox.ButtonRole.RejectRole)

        message_box.exec()

    def open_folder(self):
        """ 调用资源管理器，打开文件夹，并选中文件 """
        # print('目标路径:%s' % self.path_file)

        subprocess.Popen(f'explorer /select,"{self.path_file}"', shell=True)


class PercentileInput(QWidget):
    """ 百分位数输入窗口 """

    # 信号槽，用于返回数据
    signal_values = pyqtSignal(dict)

    def __init__(self):
        super().__init__()

        # 仅显示关闭按钮
        self.setWindowFlags(Qt.WindowType.WindowCloseButtonHint)

        # 窗口标题
        self.setWindowTitle('百分位数统计')

        # 窗口图标
        self.setWindowIcon(QIcon(cfg.path_logo))

        # 初始尺寸
        self.resize(200, 250)

        """ 主Layout """
        self.gridLayout_main = QGridLayout(self)

        ''' 百分位数GroupBox '''
        self.groupBox_percentile = QGroupBox('百分位数（0-100）')
        self.formLayout_percentile = QFormLayout(self.groupBox_percentile)
        self.formLayout_percentile.setLabelAlignment(Qt.AlignmentFlag.AlignRight)

        # PM2.5
        self.label_pm2point5 = QLabel('PM<sub>2.5</sub>')
        self.spinbox_pm2point5 = QSpinBox()
        self.spinbox_pm2point5.setRange(-1, 100)
        self.spinbox_pm2point5.setValue(-1)

        # PM10
        self.label_pm10 = QLabel('PM<sub>10</sub>')
        self.spinbox_pm10 = QSpinBox()
        self.spinbox_pm10.setRange(-1, 100)
        self.spinbox_pm10.setValue(-1)
        # self.spinbox_pm10.setSuffix(' th')

        # NO2
        self.label_no2 = QLabel('NO<sub>2</sub>')
        self.spinbox_no2 = QSpinBox()
        self.spinbox_no2.setRange(-1, 100)
        self.spinbox_no2.setValue(-1)

        # SO2
        self.label_so2 = QLabel('SO<sub>2</sub>')
        self.spinbox_so2 = QSpinBox()
        self.spinbox_so2.setRange(-1, 100)
        self.spinbox_so2.setValue(-1)

        # CO
        self.label_co = QLabel('CO')
        self.spinbox_co = QSpinBox()
        self.spinbox_co.setRange(-1, 100)
        self.spinbox_co.setValue(-1)

        # O3
        self.label_o3 = QLabel('O<sub>3</sub>')
        self.spinbox_o3 = QSpinBox(value=90)
        self.spinbox_o3.setRange(-1, 100)

        # AQI
        self.label_aqi = QLabel('AQI')
        self.spinbox_aqi = QSpinBox()
        self.spinbox_aqi.setRange(-1, 100)
        self.spinbox_aqi.setValue(-1)

        # 添加值FormLayout
        self.formLayout_percentile.addRow(self.label_pm2point5, self.spinbox_pm2point5)
        self.formLayout_percentile.addRow(self.label_pm10, self.spinbox_pm10)
        self.formLayout_percentile.addRow(self.label_no2, self.spinbox_no2)
        self.formLayout_percentile.addRow(self.label_so2, self.spinbox_so2)
        self.formLayout_percentile.addRow(self.label_co, self.spinbox_co)
        self.formLayout_percentile.addRow(self.label_o3, self.spinbox_o3)
        self.formLayout_percentile.addRow(self.label_aqi, self.spinbox_aqi)

        # 添加至主Layout
        self.gridLayout_main.addWidget(self.groupBox_percentile, 0, 0, 1, 1)

        # 说明信息
        self.label_info = QLabel('-1：忽略，50：中位数\n0：最小值，100：最大值')
        self.label_info.setWordWrap(True)
        self.gridLayout_main.addWidget(self.label_info, 1, 0, 1, 1)

        """ 按钮 """
        # 水平Layout
        self.hlayout_ = QHBoxLayout()

        # 确认按钮
        self.pbtn_yes = QPushButton('确认')
        self.pbtn_yes.setMinimumHeight(30)  # 按钮高度
        self.pbtn_yes.clicked.connect(self.get_values)  # 信号槽

        # 取消按钮
        self.pbtn_no = QPushButton('取消')
        self.pbtn_no.setMinimumHeight(30)  # 按钮高度
        self.pbtn_no.clicked.connect(self.close)  # 信号槽

        # 添加至水平Layout
        self.hlayout_.addWidget(self.pbtn_yes)
        self.hlayout_.addWidget(self.pbtn_no)

        # 添加至主Layout
        self.gridLayout_main.addLayout(self.hlayout_, 2, 0, 1, 1)

    def get_values(self):
        """ 获取每个物种的分位数值 """

        dict_percentile = dict()

        dict_percentile['PM2.5'] = self.spinbox_pm2point5.value()
        dict_percentile['PM10'] = self.spinbox_pm10.value()
        dict_percentile['O3'] = self.spinbox_o3.value()
        dict_percentile['NO2'] = self.spinbox_no2.value()
        dict_percentile['CO'] = self.spinbox_co.value()
        dict_percentile['SO2'] = self.spinbox_so2.value()
        dict_percentile['AQI'] = self.spinbox_aqi.value()

        # 删除值为-1的键值对
        dict_percentile = {key: value for key, value in dict_percentile.items() if value != -1}

        # 信号槽传回数据
        self.signal_values.emit(dict_percentile)

        # 关闭窗口
        self.close()


class SaveData(QThread, QWidget):
    """ 开启独立Thread保存数据, 防止GUI卡顿 """

    # 信号槽：状态值 {'text': , 'value': }
    signal_progress = pyqtSignal(dict)

    # 信号槽：错误信息
    signal_error = pyqtSignal(str)

    def __init__(self,
                 data: dict,  # 需要保存的数据
                 path_save: str,  # 默认保存路径
                 time_resolution: str,  # 时间分辨率
                 ):

        super().__init__()

        # 导出区域级别
        self.data = data

        # 数据保存路径
        self.path_save = path_save

        # 时间分辨率
        if time_resolution == 'H':
            self.datetime_format = '%Y-%m-%d %H:%M:%S'
        elif time_resolution == 'D':
            self.datetime_format = '%Y-%m-%d'
        elif time_resolution in ['MS', 'QS-MAR']:
            self.datetime_format = '%Y-%m'
        # elif time_resolution == 'QS-MAR':
        #     self.datetime_format = '%Y-%m'
        elif time_resolution == 'YS':
            self.datetime_format = '%Y'
        else:
            raise ValueError('时间分辨率必须为: H/D/MS/QS-MAR/YS, 你的输入: %s' % time_resolution)

    def run(self):
        """ .start()启动 """

        try:
            # 发送状态信号
            self.signal_progress.emit({'text': '保存数据...', 'value': None})

            for k in self.data.keys():
                if isinstance(self.data[k].index, pd.DatetimeIndex):
                    self.data[k].index = self.data[k].index.strftime(self.datetime_format)

            # 时间索引DatetimeIndex -> str
            # df_test.index = df_test.index.strftime(cfg.datetime_format)

            # 保存
            gkd.dict2xlsx(data=self.data, path_xlsx=self.path_save)

            # 发送状态信号
            self.signal_progress.emit({'text': '保存数据完成！', 'value': None})

            # 延迟0.5s
            self.msleep(500)

            # 发送状态信号
            self.signal_progress.emit({'text': '保存数据完成！', 'value': 1000, 'path_xlsx': self.path_save})
            # self.signal_progress.emit({'text': '保存数据完成！', 'value': 1000, 'source': 'SaveData'})

        except:
            # 错误信息返回
            self.signal_error.emit(traceback.format_exc())


if __name__ == '__main__':
    # 获取application
    app = QApplication(sys.argv)

    dpi = app.screens()[0].logicalDotsPerInch()  # 屏幕dpi
    logical_scaled_factor = dpi / 96  # 缩放比例

    # 界面风格
    app.setStyle('Fusion')

    # 初始化界面
    gkd_gui = GkdGui()
    # gkd_gui = GkdGui(logical_scaled_factor)

    # 指定错误处理函数
    sys.excepthook = gkd_gui.error_handler

    gkd_gui.show()

    # 启动GUI
    sys.exit(app.exec())
